package cn.udesk.common.toolkit;

/**
 * Copyright (C) 2008 10gen Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.net.NetworkInterface;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.Enumeration;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A globally unique identifier for objects.
 * <p>
 * Consists of 12 bytes, divided as follows: <blockquote>
 * 
 * <pre>
 * <table border="1">
 * <tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td>
 *     <td>7</td><td>8</td><td>9</td><td>10</td><td>11</td></tr>
 * <tr><td colspan="4">time</td><td colspan="3">machine</td>
 *     <td colspan="2">pid</td><td colspan="3">inc</td></tr>
 * </table>
 * </pre>
 * 
 * </blockquote>
 * 
 * @ objectids
 */
public class ObjectID implements Comparable<ObjectID>, Serializable {

  private static final long serialVersionUID = -4415279469780082174L;

  /**
   * Gets a new String id.
   * 
   * @return the new id
   */
  public static String id() {
    return new ObjectID().toString();
  }

  /**
   * Gets a new object id.
   * 
   * @return the new id
   */
  public static ObjectID get() {
    return new ObjectID();
  }

  /**
   * Checks if a string could be an <code>ObjectId</code>.
   * 
   * @return whether the string could be an object id
   */
  public static boolean isValid(String s) {
    if (s == null)
      return false;

    final int len = s.length();
    if (len != 24)
      return false;

    for (int i = 0; i < len; i++) {
      char c = s.charAt(i);
      if (c >= '0' && c <= '9')
        continue;
      if (c >= 'a' && c <= 'f')
        continue;
      if (c >= 'A' && c <= 'F')
        continue;

      return false;
    }

    return true;
  }

  /**
   * Turn an object into an <code>ObjectId</code>, if possible. Strings will be converted into
   * <code>ObjectId</code>s, if possible, and <code>ObjectId</code>s will be cast and returned.
   * Passing in <code>null</code> returns <code>null</code>.
   * 
   * @param o the object to convert
   * @return an <code>ObjectId</code> if it can be massaged, null otherwise
   */
  public static ObjectID massageToObjectId(Object o) {
    if (o == null)
      return null;

    if (o instanceof ObjectID)
      return (ObjectID) o;

    if (o instanceof String) {
      String s = o.toString();
      if (isValid(s))
        return new ObjectID(s);
    }

    return null;
  }

  public ObjectID(Date time) {
    this(time, _genmachine, _nextInc.getAndIncrement());
  }

  public ObjectID(Date time, int inc) {
    this(time, _genmachine, inc);
  }

  public ObjectID(Date time, int machine, int inc) {
    _time = (int) (time.getTime() / 1000);
    _machine = machine;
    _inc = inc;
    _new = false;
  }

  /**
   * Creates a new instance from a string.
   * 
   * @param s the string to convert
   * @throws IllegalArgumentException if the string is not a valid id
   */
  public ObjectID(String s) {
    this(s, false);
  }

  public ObjectID(String s, boolean babble) {

    if (!isValid(s))
      throw new IllegalArgumentException("invalid ObjectId [" + s + "]");

    if (babble)
      s = babbleToMongod(s);

    byte b[] = new byte[12];
    for (int i = 0; i < b.length; i++) {
      b[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
    }
    ByteBuffer bb = ByteBuffer.wrap(b);
    _time = bb.getInt();
    _machine = bb.getInt();
    _inc = bb.getInt();
    _new = false;
  }

  public ObjectID(byte[] b) {
    if (b.length != 12)
      throw new IllegalArgumentException("need 12 bytes");
    ByteBuffer bb = ByteBuffer.wrap(b);
    _time = bb.getInt();
    _machine = bb.getInt();
    _inc = bb.getInt();
    _new = false;
  }

  /**
   * Creates an ObjectId
   * 
   * @param time time in seconds
   * @param machine machine ID
   * @param inc incremental value
   */
  public ObjectID(int time, int machine, int inc) {
    _time = time;
    _machine = machine;
    _inc = inc;
    _new = false;
  }

  /**
   * Create a new object id.
   */
  public ObjectID() {
    _time = (int) (System.currentTimeMillis() / 1000);
    _machine = _genmachine;
    _inc = _nextInc.getAndIncrement();
    _new = true;
  }

  public int hashCode() {
    int x = _time;
    x += (_machine * 111);
    x += (_inc * 17);
    return x;
  }

  public boolean equals(Object o) {

    if (this == o)
      return true;

    ObjectID other = massageToObjectId(o);
    if (other == null)
      return false;

    return _time == other._time && _machine == other._machine && _inc == other._inc;
  }

  public String toStringBabble() {
    return babbleToMongod(toStringMongod());
  }

  public String toStringMongod() {
    byte b[] = toByteArray();

    StringBuilder buf = new StringBuilder(24);

    for (int i = 0; i < b.length; i++) {
      int x = b[i] & 0xFF;
      String s = Integer.toHexString(x);
      if (s.length() == 1)
        buf.append("0");
      buf.append(s);
    }

    return buf.toString();
  }

  public byte[] toByteArray() {
    byte b[] = new byte[12];
    ByteBuffer bb = ByteBuffer.wrap(b);
    // by default BB is big endian like we need
    bb.putInt(_time);
    bb.putInt(_machine);
    bb.putInt(_inc);
    return b;
  }

  static String _pos(String s, int p) {
    return s.substring(p * 2, (p * 2) + 2);
  }

  public static String babbleToMongod(String b) {
    if (!isValid(b))
      throw new IllegalArgumentException("invalid object id: " + b);

    StringBuilder buf = new StringBuilder(24);
    for (int i = 7; i >= 0; i--)
      buf.append(_pos(b, i));
    for (int i = 11; i >= 8; i--)
      buf.append(_pos(b, i));

    return buf.toString();
  }

  public String toString() {
    return toStringMongod();
  }

  int _compareUnsigned(int i, int j) {
    long li = 0xFFFFFFFFL;
    li = i & li;
    long lj = 0xFFFFFFFFL;
    lj = j & lj;
    long diff = li - lj;
    if (diff < Integer.MIN_VALUE)
      return Integer.MIN_VALUE;
    if (diff > Integer.MAX_VALUE)
      return Integer.MAX_VALUE;
    return (int) diff;
  }

  public int compareTo(ObjectID id) {
    if (id == null)
      return -1;

    int x = _compareUnsigned(_time, id._time);
    if (x != 0)
      return x;

    x = _compareUnsigned(_machine, id._machine);
    if (x != 0)
      return x;

    return _compareUnsigned(_inc, id._inc);
  }

  public int getMachine() {
    return _machine;
  }

  /**
   * Gets the time of this ID, in milliseconds
   */
  public long getTime() {
    return _time * 1000L;
  }

  /**
   * Gets the time of this ID, in seconds
   */
  public int getTimeSecond() {
    return _time;
  }

  public int getInc() {
    return _inc;
  }

  public int _time() {
    return _time;
  }

  public int _machine() {
    return _machine;
  }

  public int _inc() {
    return _inc;
  }

  public boolean isNew() {
    return _new;
  }

  public void notNew() {
    _new = false;
  }

  /**
   * Gets the generated machine ID, identifying the machine / process / class loader
   */
  public static int getGenMachineId() {
    return _genmachine;
  }

  /**
   * Gets the current value of the auto increment
   */
  public static int getCurrentInc() {
    return _nextInc.get();
  }

  final int _time;
  final int _machine;
  final int _inc;

  boolean _new;

  public static int _flip(int x) {
    int z = 0;
    z |= ((x << 24) & 0xFF000000);
    z |= ((x << 8) & 0x00FF0000);
    z |= ((x >> 8) & 0x0000FF00);
    z |= ((x >> 24) & 0x000000FF);
    return z;
  }

  private static AtomicInteger _nextInc = new AtomicInteger((new Random()).nextInt());

  private static final int _genmachine;
  static {
    try {
      // build a 2-byte machine piece based on NICs info
      int machinePiece;
      {
        try {
          StringBuilder sb = new StringBuilder();
          Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
          while (e.hasMoreElements()) {
            NetworkInterface ni = e.nextElement();
            sb.append(ni.toString());
          }
          machinePiece = sb.toString().hashCode() << 16;
        } catch (Exception e) {
          // exception sometimes happens with IBM JVM, use random
          machinePiece = (new Random().nextInt()) << 16;
        }
      }
      // add a 2 byte process piece. It must represent not only the JVM
      // but the class loader.
      // Since static var belong to class loader there could be collisions
      // otherwise
      final int processPiece;
      {
        int processId = new Random().nextInt();
        try {
          processId = ManagementFactory.getRuntimeMXBean().getName().hashCode();
        } catch (Exception e) {
        }
        ClassLoader loader = ObjectID.class.getClassLoader();
        int loaderId = loader != null ? System.identityHashCode(loader) : 0;

        StringBuilder sb = new StringBuilder();
        sb.append(Integer.toHexString(processId));
        sb.append(Integer.toHexString(loaderId));
        processPiece = sb.toString().hashCode() & 0xFFFF;
      }
      _genmachine = machinePiece | processPiece;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

}
